Sustainsys
← Sustainsys AI Academy MS Graph API — Python Flask
0%
Overview

MS Graph API with Python Flask

A hands-on guide to connecting your web app to Microsoft 365 — reading emails, calendars, and more using the Microsoft Graph API.

What is Microsoft Graph?

Microsoft Graph is a single REST API that lets your app access data across Microsoft 365 — Outlook emails, calendar events, OneDrive files, Teams messages, and more. Instead of talking to each service separately, everything goes through one endpoint: https://graph.microsoft.com

How the Login Flow Works

Before we write any code, understand what happens when a user clicks "Sign in with Microsoft":

👤 User clicks Login
→
🐍 Flask redirects
→
🔐 Microsoft Login Page
→
✅ User grants consent
→
🎫 Flask gets auth code
→
🔑 Exchanges for token
→
📧 Call Graph API

This is called the Authorization Code Flow. Your app never sees the user's password — it only gets a token that grants access to specific data (like emails) that the user consented to.

What We'll Build

By the end of this guide you'll have a working Flask app that:

  • Lets users sign in with their Microsoft account
  • Reads their recent emails from Outlook
  • Reads their upcoming calendar events
  • Displays everything in a clean web interface

Prerequisites

RequirementDetails
Python3.10+ — you have 3.11.9 via pyenv
Microsoft AccountAny M365 / Outlook.com account
Azure App Registration✅ You've already created this
Code EditorVS Code or any editor
Step 1

Configure Authentication in Azure

Set up the redirect URI so Microsoft knows where to send users back after they sign in.

📍 Where You Are
You're in Azure Portal → App Registrations → Sustainsys_Learn → Authentication. This is exactly the right place.

Add a Redirect URI

Click "Add a platform" (or "Add Redirect URI")
In the panel that slides out, select "Web" (not SPA, not Mobile)
In the Redirect URI field, enter:
http://localhost:5000/getAToken
This is the URL Flask will listen on to receive the auth code from Microsoft.
Leave Front-channel logout URL blank
Under Implicit grant and hybrid flows, check ID tokens only. Leave Access tokens unchecked.
Click "Configure"
⚠️ Why /getAToken?
This is the path the MSAL Python library uses by default in its Flask integration. Your Flask app will have a route at this exact path to handle the callback. The URI must match exactly — including the case and no trailing slash.

Verify API Permissions

Go to API Permissions in the left sidebar and make sure these are listed:

PermissionTypeWhat It Does
User.ReadDelegatedRead user's basic profile
Mail.ReadDelegatedRead user's email
Calendars.ReadDelegatedRead user's calendar events

If Mail.Read and Calendars.Read aren't there yet, click "Add a permission" → Microsoft Graph → Delegated permissions, search for each, tick them, and click "Add permissions".

💡 Delegated vs Application
Delegated = acts on behalf of a signed-in user (what we're doing). Application = acts as the app itself with no user (used for background services/daemons).

Note Your IDs

Go to Overview and copy these two values — you'll need them in Step 3:

FieldWhere to Find It
Application (client) IDOverview page, top section
Directory (tenant) IDOverview page, top section
Step 2

Create a Client Secret

Your app needs a secret password to prove its identity when exchanging the auth code for a token.

Go to Certificates & secrets in the left sidebar
Click "+ New client secret"
Description: Flask Dev Secret (or anything descriptive)
Expires: 6 months (fine for learning)
Click "Add"
🚨 CRITICAL — Copy the Value Immediately!
After clicking Add, the Value column shows the secret. Copy it right now. Once you leave this page, you'll never see it again. The "Secret ID" is NOT the secret — you need the Value.

You now have three pieces of info. Keep them handy:

WhatLooks Like
Client IDxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Tenant IDxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Client SecretaBcDeFg~xYz123456789...
Step 3

Set Up the Python Project

Create the project folder, virtual environment, install dependencies, and configure your secrets.

Project Structure

ms-graph-flask/
├── app.py             # Main Flask application
├── app_config.py      # Configuration (reads .env)
├── .env                # Your secrets (never commit!)
├── requirements.txt   # Python dependencies
└── templates/
    ├── index.html      # Home / login page
    ├── display.html    # Show Graph API results
    └── auth_error.html # Error page

Terminal Commands

Terminal
# Create project folder
mkdir ms-graph-flask
cd ms-graph-flask

# Create virtual environment
python -m venv venv

# Activate it
# On Mac/Linux:
source venv/bin/activate
# On Windows:
venv\Scripts\activate

# Install dependencies
pip install flask identity requests python-dotenv
📦 What are these packages?
flask — web framework
identity — Microsoft's official MSAL wrapper for Flask (makes auth super easy)
requests — HTTP client for calling Graph API
python-dotenv — loads your .env secrets

Create the .env File

.env
# Replace with YOUR values from Azure Portal
CLIENT_ID=paste-your-client-id-here
CLIENT_SECRET=paste-your-client-secret-value-here
AUTHORITY=https://login.microsoftonline.com/paste-your-tenant-id-here
⚠️ Never commit .env to Git
Add .env to your .gitignore file. These secrets must stay local.

Create app_config.py

app_config.py
import os
from dotenv import load_dotenv

load_dotenv()  # reads .env file

CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
AUTHORITY = os.getenv("AUTHORITY")

# Where Microsoft redirects after login
REDIRECT_PATH = "/getAToken"

# What data your app wants to access
SCOPE = ["User.Read", "Mail.Read", "Calendars.Read"]

# Graph API base URL
ENDPOINT = "https://graph.microsoft.com/v1.0"
Step 4

Write the Flask Application

This is the core of your app — handling login, callback, and Graph API calls.

app.py
import identity.web
import requests
from flask import Flask, redirect, render_template, request, session, url_for
import app_config

# ─── Create Flask app ───
app = Flask(__name__)
app.config["SECRET_KEY"] = "super-secret-dev-key-change-in-production"

# ─── Set up Microsoft authentication ───
auth = identity.web.Auth(
    session=session,
    authority=app_config.AUTHORITY,
    client_id=app_config.CLIENT_ID,
    client_credential=app_config.CLIENT_SECRET,
)


# ─── HOME PAGE ───
@app.route("/")
def index():
    if not (app.config["SECRET_KEY"]):
        return render_template("config_error.html")
    return render_template(
        "index.html",
        user=auth.get_user(),
        # This generates the Microsoft login URL
        auth_url=auth.log_in(
            scopes=app_config.SCOPE,
            redirect_uri=url_for("auth_response", _external=True),
        ),
    )


# ─── CALLBACK: Microsoft redirects here after login ───
@app.route(app_config.REDIRECT_PATH)
def auth_response():
    result = auth.complete_log_in(request.args)
    if "error" in result:
        return render_template("auth_error.html", result=result)
    return redirect(url_for("index"))


# ─── LOGOUT ───
@app.route("/logout")
def logout():
    return redirect(auth.log_out(url_for("index", _external=True)))


# ─── CALL GRAPH API (generic helper) ───
def call_graph(endpoint):
    """Call Microsoft Graph with the user's token."""
    token = auth.get_token_for_user(app_config.SCOPE)
    if "error" in token:
        return redirect(url_for("index"))
    response = requests.get(
        endpoint,
        headers={"Authorization": "Bearer " + token["access_token"]},
    ).json()
    return response


# ─── READ EMAILS ───
@app.route("/emails")
def emails():
    user = auth.get_user()
    if not user:
        return redirect(url_for("index"))
    # Get top 10 emails from inbox
    data = call_graph(
        f"{app_config.ENDPOINT}/me/messages?$top=10&$select=subject,from,receivedDateTime,bodyPreview"
    )
    return render_template("display.html", user=user, data=data, title="Emails")


# ─── READ CALENDAR ───
@app.route("/calendar")
def calendar():
    user = auth.get_user()
    if not user:
        return redirect(url_for("index"))
    # Get next 10 upcoming events
    data = call_graph(
        f"{app_config.ENDPOINT}/me/events?$top=10&$select=subject,start,end,location,organizer"
    )
    return render_template("display.html", user=user, data=data, title="Calendar")


# ─── RUN ───
if __name__ == "__main__":
    app.run(debug=True, port=5000)

Code Walkthrough

Let's break down what each part does:

identity.web.Auth

This is Microsoft's official helper. It handles the entire OAuth flow for you — generating login URLs, exchanging codes for tokens, refreshing expired tokens, and storing everything in the Flask session.

auth.log_in()

Generates the URL that sends the user to Microsoft's login page. The scopes parameter tells Microsoft what permissions your app needs (emails, calendar, etc.).

auth.complete_log_in()

Called when Microsoft redirects back to your app. It validates the response, exchanges the authorization code for access + refresh tokens, and stores them in the session.

call_graph()

Our helper function that gets a valid token and makes GET requests to the Graph API. The token goes in the Authorization: Bearer header — this is how every Graph API call is authenticated.

Step 5

Create the HTML Templates

Three simple Jinja2 templates to display the login page, data, and errors.

templates/index.html

templates/index.html
<!DOCTYPE html>
<html>
<head>
    <title>MS Graph Flask App</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; max-width: 800px;
               margin: 40px auto; padding: 0 20px; background: #f5f5f5; }
        .card { background: white; border-radius: 8px; padding: 32px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 20px 0; }
        h1 { color: #0078d4; }
        .btn { display: inline-block; padding: 12px 24px; background: #0078d4;
               color: white; text-decoration: none; border-radius: 6px;
               font-weight: 600; margin: 8px 4px; }
        .btn:hover { background: #106ebe; }
        .btn-danger { background: #d83b01; }
        .btn-danger:hover { background: #c23501; }
        .welcome { color: #107c10; font-size: 18px; }
    </style>
</head>
<body>
    <div class="card">
        <h1>📊 MS Graph API Explorer</h1>

        {% if user %}
            <p class="welcome">
                ✅ Signed in as <strong>{{ user.get("name", "Unknown") }}</strong>
            </p>
            <p>What would you like to explore?</p>
            <a href="/emails" class="btn">📧 Read My Emails</a>
            <a href="/calendar" class="btn">📅 View My Calendar</a>
            <br><br>
            <a href="/logout" class="btn btn-danger">Sign Out</a>
        {% else %}
            <p>Sign in with your Microsoft account to explore the Graph API.</p>
            <a href="{{ auth_url }}" class="btn">🔐 Sign in with Microsoft</a>
        {% endif %}
    </div>
</body>
</html>

templates/display.html

templates/display.html
<!DOCTYPE html>
<html>
<head>
    <title>{{ title }} — MS Graph</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; max-width: 900px;
               margin: 40px auto; padding: 0 20px; background: #f5f5f5; }
        .card { background: white; border-radius: 8px; padding: 24px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 12px 0; }
        h1 { color: #0078d4; }
        .item { border-left: 3px solid #0078d4; padding: 12px 16px;
                margin: 8px 0; background: #fafafa; border-radius: 0 6px 6px 0; }
        .item h3 { margin: 0 0 4px; color: #333; }
        .item p { margin: 2px 0; color: #666; font-size: 14px; }
        .back { display: inline-block; padding: 10px 20px; background: #0078d4;
                color: white; text-decoration: none; border-radius: 6px;
                margin-top: 16px; }
        .raw { background: #1e1e1e; color: #d4d4d4; padding: 16px;
               border-radius: 8px; font-family: monospace; font-size: 12px;
               white-space: pre-wrap; overflow-x: auto; margin-top: 16px;
               max-height: 300px; overflow-y: auto; }
    </style>
</head>
<body>
    <h1>{{ title }}</h1>
    <p>Signed in as <strong>{{ user.get("name", "Unknown") }}</strong></p>

    {% if data and data.get("value") %}
        {% for item in data["value"] %}
        <div class="item">
            {% if title == "Emails" %}
                <h3>{{ item.get("subject", "(No subject)") }}</h3>
                <p>📨 From: {{ item.get("from", {}).get("emailAddress", {}).get("address", "Unknown") }}</p>
                <p>🕒 {{ item.get("receivedDateTime", "")[:16] }}</p>
                <p>{{ item.get("bodyPreview", "")[:120] }}...</p>
            {% elif title == "Calendar" %}
                <h3>{{ item.get("subject", "(No subject)") }}</h3>
                <p>📅 {{ item.get("start", {}).get("dateTime", "")[:16] }}
                   → {{ item.get("end", {}).get("dateTime", "")[:16] }}</p>
                <p>📍 {{ item.get("location", {}).get("displayName", "No location") }}</p>
            {% endif %}
        </div>
        {% endfor %}
    {% else %}
        <div class="card">
            <p>No {{ title.lower() }} found, or there was an error.</p>
        </div>
    {% endif %}

    <!-- Show raw JSON for learning -->
    <details>
        <summary style="cursor:pointer; margin-top:20px; color:#0078d4; font-weight:600;">
            🔍 View Raw JSON Response (for learning)
        </summary>
        <div class="raw">{{ data | tojson(indent=2) }}</div>
    </details>

    <a href="/" class="back">← Back Home</a>
</body>
</html>

templates/auth_error.html

templates/auth_error.html
<!DOCTYPE html>
<html>
<head><title>Auth Error</title></head>
<body style="font-family: 'Segoe UI', sans-serif; max-width: 600px; margin: 60px auto; text-align: center;">
    <h1 style="color: #d83b01;">⚠️ Authentication Error</h1>
    <p><strong>Error:</strong> {{ result.get("error") }}</p>
    <p><strong>Description:</strong> {{ result.get("error_description") }}</p>
    <a href="/" style="display:inline-block; margin-top:20px; padding:10px 20px; background:#0078d4; color:white; text-decoration:none; border-radius:6px;">
        Try Again
    </a>
</body>
</html>
Step 6

Run Your App & Sign In

Moment of truth — let's fire up the Flask server and sign in with Microsoft.

Start the Server

Terminal
# Make sure you're in the project folder with venv active
cd ms-graph-flask
source venv/bin/activate  # or venv\Scripts\activate on Windows

# Run!
python app.py

You should see:

 * Running on http://127.0.0.1:5000
 * Debug mode: on

Test the Flow

Open http://localhost:5000 in your browser
Click "Sign in with Microsoft"
You'll be redirected to Microsoft's login page — enter your credentials
Grant the permissions when prompted (first time only)
You'll be redirected back to your app — now signed in! 🎉
🔧 Common Issues
Redirect URI mismatch error?
Go back to Azure → Authentication and check the redirect URI is exactly http://localhost:5000/getAToken (no trailing slash, lowercase, http not https).

AADSTS65001 consent error?
Go to Azure → API Permissions → click "Grant admin consent for [your org]" if you see this button.

Module not found?
Make sure your virtual environment is activated and you ran pip install flask identity requests python-dotenv.
Step 7

Read Your Emails

Click "Read My Emails" in your app — let's understand what's happening under the hood.

The Graph API Call

When you click the emails button, your app makes this HTTP request:

GET https://graph.microsoft.com/v1.0/me/messages?$top=10&$select=subject,from,receivedDateTime,bodyPreview

Headers:
  Authorization: Bearer eyJ0eXAi...  (your access token)

Breaking Down the URL

PartMeaning
/meThe signed-in user
/messagesTheir email messages
$top=10Return only the first 10 results
$select=...Only return these fields (faster response)
💡 OData Query Parameters
Graph API uses OData query syntax. Other useful ones:
$filter — filter results (e.g., unread only)
$orderby — sort results
$search — full-text search
$count — include total count

Example: /me/messages?$filter=isRead eq false&$top=5 returns only unread emails.

Explore the Raw JSON

Click "View Raw JSON Response" in the display page. This is the actual Graph API response. Notice the structure:

{
    "@odata.context": "...",
    "value": [
        {
            "subject": "Meeting Tomorrow",
            "from": {
                "emailAddress": {
                    "name": "John Doe",
                    "address": "[email protected]"
                }
            },
            "receivedDateTime": "2026-03-25T10:30:00Z",
            "bodyPreview": "Hi, just confirming..."
        },
        // ... more emails
    ]
}

The value array contains all the emails. Each email is a JSON object with the fields you asked for in $select.

Step 8

Read Your Calendar

Same pattern, different endpoint — that's the beauty of Graph API.

The Calendar API Call

GET https://graph.microsoft.com/v1.0/me/events?$top=10&$select=subject,start,end,location,organizer
PartMeaning
/me/eventsThe signed-in user's calendar events
start, endEvent time range (includes timezone)
locationWhere the meeting is
organizerWho created the event
💡 Calendar View vs Events
/me/events returns all events. For a date-range view (like "this week"), use:
/me/calendarView?startDateTime=2026-03-25T00:00:00&endDateTime=2026-03-31T23:59:59
This is more like how a calendar app works.

The Key Pattern

Notice something? Every Graph call follows the exact same pattern:

Get Token
→
GET /me/{resource}
→
JSON Response
→
Display Data

That's it. The call_graph() function in your app handles this pattern. To access any new resource, you just change the endpoint URL. The auth stays the same.

Complete

What's Next?

You've built a working MS Graph app! Here are your next learning paths.

Try These Graph Endpoints

Add new routes to your Flask app using the same call_graph() pattern:

WhatEndpointPermission Needed
Your profile photo/me/photo/$valueUser.Read
OneDrive files/me/drive/root/childrenFiles.Read
Send an emailPOST /me/sendMailMail.Send
Teams chats/me/chatsChat.Read
Contacts/me/contactsContacts.Read

Remember to add the required permissions in Azure → API Permissions before using new endpoints.

Deeper Learning

  • Graph Explorer — developer.microsoft.com/graph/graph-explorer — test any endpoint in the browser before writing code
  • Batch requests — call multiple endpoints in one HTTP request
  • Webhooks / Change Notifications — get notified when data changes (new email arrives)
  • Delta queries — only fetch data that changed since your last request
  • Application permissions — build background services that run without a user

Integrate with Your AI Stack

Now that you understand Graph API, consider connecting it to your agentic AI work:

  • MCP Server — Build a Graph API MCP server so your LangGraph agents can read emails and calendars
  • RAG Pipeline — Index Outlook emails or OneDrive documents into ChromaDB for retrieval
  • Multi-Agent System — Add a "Microsoft 365 Agent" to your LangGraph orchestrator alongside your Gmail agent
🎉 Well Done!
You've gone from zero to a working MS Graph integration. The core pattern — register app, get token, call API — is the same across all of Microsoft 365. You now have the foundation to build anything with Graph API.